﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Net;
using IndicoInterface;
using Utilities;

namespace IndicoInterface
{
    /// <summary>
    /// Given a standard conference agenda URL will give and cache information about that agenda - the full
    /// time table.
    /// </summary>
    public class AgendaInfo
    {
        /// <summary>
        /// The main URL of the conference.
        /// </summary>
        public string ConferenceID { get; set; }

        /// <summary>
        /// The web site where the agenda is located.
        /// </summary>
        public string AgendaSite { get; set; }

        /// <summary>
        /// Returns the agenda sub directory
        /// </summary>
        public string AgendaSubDirectory { get; set; }

        /// <summary>
        /// Do not use this - useful only for sereialization.
        /// </summary>
        public AgendaInfo()
        {
            AgendaSubDirectory = "";
        }

        /// <summary>
        /// Given a standard conference URL like "http://indico.cern.ch/conferenceDisplay.py?confId=14475",
        /// this will be ready to load up the required information.
        /// </summary>
        /// <param name="agendaUrl"></param>
        public AgendaInfo(string agendaUrl)
        {
            ///
            /// Get the conference ID out of the URL
            /// 

            int start = agendaUrl.IndexOf("confId=");
            if (start < 0)
            {
                throw new AgendaException("URL does not have a confId parameter!");
            }
            string id = agendaUrl.Substring(start + 7);
            int idend = id.IndexOfAny(new char[] {'&', '#'});
            if (idend > 0)
            {
                id = id.Substring(0, idend);
            }
            ConferenceID = id;

            ///
            /// Get the agenda site out of the URL
            ///

            int leaderLen = 0;
            if (agendaUrl.StartsWith("http://")) {
                leaderLen = 7;
            }
            else if (agendaUrl.StartsWith("https://"))
            {
                leaderLen = 8;
            }
            else
            {
                throw new AgendaException("URL for agenda does not start with http or https!");
            }

            string restOfAgenda = agendaUrl.Substring(leaderLen);
            int nextslash = restOfAgenda.IndexOf("/");
            AgendaSite = restOfAgenda.Substring(0, nextslash);

            ///
            /// Next, we have to see if there is any subdirectory in the agneda guy!
            /// 

            AgendaSubDirectory = "";
            int lastSlash = restOfAgenda.LastIndexOf("/");
            if (lastSlash != nextslash)
            {
                AgendaSubDirectory = restOfAgenda.Substring(nextslash+1, lastSlash - nextslash - 1);
            }
        }

        /// <summary>
        /// Use if you already know the conference ID. Defaults to CERN agenda site.
        /// </summary>
        /// <param name="confID"></param>
        public AgendaInfo(int confID)
        {
            ConferenceID = confID.ToString();
            AgendaSite = "indico.cern.ch";
        }

        /// <summary>
        /// Agenda from a different agenda site.
        /// </summary>
        /// <param name="confID"></param>
        /// <param name="agendaSite"></param>
        public AgendaInfo(int confID, string agendaSite)
        {
            ConferenceID = confID.ToString();
            AgendaSite = agendaSite;
        }

        /// <summary>
        /// Returns the URL of the full XML of the conference. Not often used by
        /// someone outside of this guy, but very helpful for testing.
        /// </summary>
        public string AgendaFullXML
        {
            get
            {
                StringBuilder bld = new StringBuilder();
                bld.AppendFormat("http://{0}/", AgendaSite);
                if (AgendaSubDirectory != "")
                {
                    bld.AppendFormat("{0}/", AgendaSubDirectory);
                }
                bld.AppendFormat("conferenceOtherViews.py?confId={0}&view=xml&showDate=all&showSession=all&detailLevel=contribution&fr=no", ConferenceID);
                return bld.ToString();
            }
        }

        /// <summary>
        /// Track the loader for this conference.
        /// </summary>
        XmlSerializer _loader = null;

        /// <summary>
        /// Returns the full conference agenda data.
        /// </summary>
        /// <returns></returns>
        public IndicoDataModel.iconf GetFullConferenceData()
        {
            ///
            /// Create the deserializer if need be
            ///

            if (_loader == null)
            {
                _loader = new XmlSerializer(typeof(IndicoDataModel.iconf));
            }

            ///
            /// Now, get it!
            /// 

            Logger.LogErrorMessage("AgendaDownload", "Downloading " + ToString() + " - " + AgendaFullXML);
            WebRequest req = WebRequest.Create(AgendaFullXML);
            using (WebResponse res = req.GetResponse())
            {
                using (TextReader r = new StreamReader(res.GetResponseStream()))
                {
                    IndicoDataModel.iconf data = _loader.Deserialize(r) as IndicoDataModel.iconf;
                    res.Close();
                    return data;
                }
            }
        }

        /// <summary>
        /// Return the conference data - only do it in the simplified and normalized
        /// format.
        /// </summary>
        /// <returns></returns>
        public SimpleAgendaDataModel.Meeting GetNormalizedConferenceData()
        {
            ///
            /// Grab all the details
            ///

            var data = GetFullConferenceData();

            ///
            /// Create the stuff we will be sending back.
            /// 

            SimpleAgendaDataModel.Meeting m = new IndicoInterface.SimpleAgendaDataModel.Meeting();
            m.ID = data.ID;
            m.Title = data.title;
            m.Site = AgendaSite;
            m.StartDate = AgendaStringToDate(data.startDate);
            m.EndDate = AgendaStringToDate(data.endDate);

            ///
            /// Convert all the session and talks into our simple data structure.
            /// We can tell the difference between a meeting and a conference by
            /// looking to see if there are any sessions or contributions. One of
            /// them will be null.
            /// 

            if (data.session != null)
            {
                ///
                /// We have a conference.
                /// 

                m.Sessions = ParseConference(data.session);
            }
            else
            {
                ///
                /// We have a single session meeting.
                /// 

                IndicoInterface.SimpleAgendaDataModel.Session s = ParseSingleMeeting(data.contribution, "0", data.title, data.startDate, data.endDate);
                m.Sessions = new IndicoInterface.SimpleAgendaDataModel.Session[1] { s };
            }

            return m;
        }

        /// <summary>
        /// Look through the contribution list and extract the talk info.
        /// </summary>
        /// <param name="contribution"></param>
        /// <returns></returns>
        private IndicoInterface.SimpleAgendaDataModel.Session ParseSingleMeeting(IndicoInterface.IndicoDataModel.contribution[] contribution, string ID, string title, string startTime, string endTime)
        {
            IndicoInterface.SimpleAgendaDataModel.Session result = new IndicoInterface.SimpleAgendaDataModel.Session();
            result.ID = ID;
            result.Title = title;
            result.StartDate = AgendaStringToDate(startTime);
            result.EndDate = AgendaStringToDate(endTime);

            /// If there are no contributions to thsi session (WEIRD) then return an empty list.
            if (contribution == null)
            {
                result.Talks = new IndicoInterface.SimpleAgendaDataModel.Talk[0];
            }
            else
            {
                /// Transform each contribution into a talk item.
                var alltalks = from t in contribution
                               select ExtractTalkInfo(t);
                result.Talks = alltalks.ToArray();
            }
            return result;
        }

        /// <summary>
        /// Convert a string time from the agenda server to the proper time.
        /// </summary>
        /// <param name="endTime"></param>
        /// <returns></returns>
        private DateTime AgendaStringToDate(string datetime)
        {
            return DateTime.Parse(datetime);
        }

        /// <summary>
        /// Given a contribution, return a talk.
        /// </summary>
        /// <param name="contrib"></param>
        /// <returns></returns>
        private IndicoInterface.SimpleAgendaDataModel.Talk ExtractTalkInfo(IndicoInterface.IndicoDataModel.contribution contrib)
        {
            IndicoInterface.SimpleAgendaDataModel.Talk result = new IndicoInterface.SimpleAgendaDataModel.Talk();
            result.ID = contrib.ID;
            result.Title = contrib.title;
            result.StartDate = AgendaStringToDate(contrib.startDate);
            result.EndDate = AgendaStringToDate(contrib.endDate);

            if (contrib.speakers != null)
            {
                var speakers = from s in contrib.speakers
                               from u in s.users
                               select (u.name.first + " " + (u.name.middle + " " + u.name.last).Trim()).Trim();
                result.Speakers = speakers.ToArray();
            }
            else
            {
                result.Speakers = new string[0];
            }

            foreach (var materialType in new string[] {"slides", "transparencies", "poster", "0"})
            {
                result.SlideURL = FindMaterial(contrib.material, materialType);
                if (result.SlideURL != null)
                {
                    break;
                }
            }
            if (result.SlideURL == null)
            {
                Logger.LogErrorMessage("NoTalkData", "No usable talk slides or poster found for " + result.Title);
            }

            return result;
        }

        /// <summary>
        /// Given material for a particular contribution, find the appropriate type, and fetch out
        /// a URL for the actual data.
        /// </summary>
        /// <param name="material"></param>
        /// <param name="p"></param>
        /// <returns></returns>
        private string FindMaterial(IndicoInterface.IndicoDataModel.material[] materiallist, string material_type)
        {
            ///
            /// If no one uploaded anything, then we won't be sending anything back. :(
            ///

            if (materiallist == null)
            {
                return null;
            }

            ///
            /// See if we can find material of the correct type.
            /// 

            var good = from m in materiallist
                       where m.ID == material_type
                       select m;

            /// First good is returned!
            foreach (var materialType in new string[] { "pptx", "ppt", "ps", "pdf" })
            {
                string result = SearchMaterialListForType(good, materialType);
                if (result != null)
                {
                    return result;
                }
            }
            return null;
        }

        /// <summary>
        /// Scan for a particular material type the list of items in teh indigo model.
        /// </summary>
        /// <param name="good"></param>
        /// <returns></returns>
        private static string SearchMaterialListForType(IEnumerable<IndicoInterface.IndicoDataModel.material> good, string materialType)
        {
            foreach (var item in good)
            {
                /// Anything good uploaded to the agenda server?
                if (item.files != null && item.files.file != null)
                {
                    var filename = item.files.file.FirstOrDefault(f => f.type.ToLower() == materialType);
                    if (filename != null
                        && filename.url != null)
                    {
                        return filename.url;
                    }
                }

                /// An external link?
                if (item.link != null && item.link.ToLower().EndsWith(materialType))
                {
                    return item.link;
                }

                /// An explicit link - ugly!!?
                if (materialType == "pdf" && item.pdf != null) {
                    return item.pdf;
                }
                if (materialType == "pptx" && item.pptx != null)
                {
                    return item.pptx;
                }
                if (materialType == "ps" && item.ps != null)
                {
                    return item.ps;
                }
                if (materialType == "ppt" && item.ppt != null)
                {
                    return item.ppt;
                }
            }

            /// Found nothing!
            return null;
        }

        /// <summary>
        /// Given a list of sessions, return a list of normalized sessions.
        /// </summary>
        /// <param name="session">The list of sessiosn -- can't be null, but could be empty.</param>
        /// <returns></returns>
        private IndicoInterface.SimpleAgendaDataModel.Session[] ParseConference(IndicoInterface.IndicoDataModel.session[] session)
        {
            /// LINQ makes this transformation so simple. Much worse in C++!!!
            var allsessions = from s in session
                              select ParseSingleMeeting(s.contribution, s.ID, s.title, s.startDate, s.endDate);
            return allsessions.ToArray();
        }

        /// <summary>
        /// Returns a printable string about what this thing is.
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return "Agenda at " + AgendaSite + " with conference ID " + ConferenceID.ToString();
        }

        /// <summary>
        /// Code to seralize this guy to an xml dude.
        /// </summary>
        /// <param name="sw"></param>
        public void Seralize(TextWriter sw)
        {
            XmlSerializer ser = new XmlSerializer(typeof(AgendaInfo));
            ser.Serialize(sw, this);
        }

        /// <summary>
        /// Create an agenda info from XML.
        /// </summary>
        /// <param name="rdr"></param>
        /// <returns></returns>
        public static AgendaInfo Deseralize(TextReader rdr)
        {
            XmlSerializer ser = new XmlSerializer(typeof(AgendaInfo));
            return ser.Deserialize(rdr) as AgendaInfo;
        }

        /// <summary>
        /// Desearlize from a string
        /// </summary>
        /// <param name="p"></param>
        /// <returns></returns>
        public static AgendaInfo Deseralize(string p)
        {
            using (StringReader rdr = new StringReader(p))
            {
                return Deseralize(rdr);
            }
        }
    }
}
